@cdellacqua/signals
A simple signal pattern implementation that enables reactive programming.
Signals are event emitters with specific purposes. For example:
button.addEventListener('click', () => console.log('click'));
input.addEventListener('change', (e) => console.log(e));
...could be rewritten with signals as:
button.clicked.subscribe(() => console.log('click'));
input.changed.subscribe((e) => console.log(e));
NPM Package
npm install @cdellacqua/signals
Documentation
Highlights
Signal<T>
provides methods such as:
emit(value)
, to emit a value to all subscribers;subscribe(subscriber)
, to attach subscribers;subscribeOnce(subscriber)
, to attach subscribers for a single emit
call.
When you subscribe to a signal, you get a unsubscribe function, e.g.:
import {makeSignal} from '@cdellacqua/signals';
const signal$ = makeSignal<number>();
const unsubscribe = signal$.subscribe((v) => console.log(v));
signal$.emit(3.14);
unsubscribe();
signal$.emit(42);
The above code can be rewritten with subscribeOnce
:
import {makeSignal} from '@cdellacqua/signals';
const signal$ = makeSignal<number>();
signal$.subscribeOnce((v) => console.log(v));
signal$.emit(3.14);
signal$.emit(42);
Signal<T>
also contains a getter (nOfSubscriptions
) that lets you know how many active subscriptions
are active at a given moment (this could be useful if you are trying to optimize your code).
import {makeSignal} from '@cdellacqua/signals';
const signal$ = makeSignal<number>();
console.log(signal$.nOfSubscriptions);
const unsubscribe = signal$.subscribe(() => undefined);
console.log(signal$.nOfSubscriptions);
unsubscribe();
console.log(signal$.nOfSubscriptions);
A nice feature of Signal<T>
is that it deduplicates subscribers,
that is you can't accidentally add the same function more than
once to the same signal (just like the DOM addEventListener method):
import {makeSignal} from '@cdellacqua/signals';
const signal$ = makeSignal<number>();
const subscriber = (v: number) => console.log(v);
console.log(signal$.nOfSubscriptions);
const unsubscribe1 = signal$.subscribe(subscriber);
const unsubscribe2 = signal$.subscribe(subscriber);
const unsubscribe3 = signal$.subscribe(subscriber);
console.log(signal$.nOfSubscriptions);
unsubscribe3();
unsubscribe2();
unsubscribe1();
console.log(signal$.nOfSubscriptions);
If you ever needed to add the same function
more than once you can still achieve that by simply wrapping it inside an arrow function:
import {makeSignal} from '@cdellacqua/signals';
const signal$ = makeSignal<number>();
const subscriber = (v: number) => console.log(v);
console.log(signal$.nOfSubscriptions);
const unsubscribe1 = signal$.subscribe(subscriber);
console.log(signal$.nOfSubscriptions);
const unsubscribe2 = signal$.subscribe((v) => subscriber(v));
console.log(signal$.nOfSubscriptions);
unsubscribe2();
console.log(signal$.nOfSubscriptions);
unsubscribe1();
console.log(signal$.nOfSubscriptions);
You can also have a signal that just triggers its subscribers without passing
any data:
import {makeSignal} from '@cdellacqua/signals';
const signal$ = makeSignal<void>();
signal$.emit();
Coalescing and deriving signals
Coalescing
Coalescing multiple signals into one consists of
creating a new signal that will emit the latest value emitted by any source
signal.
Example:
import {makeSignal, coalesceSignals} from '@cdellacqua/signals';
const lastUpdate1$ = makeSignal<number>();
const lastUpdate2$ = makeSignal<number>();
const latestUpdate$ = coalesceSignals([lastUpdate1$, lastUpdate2$]);
latestUpdate$.subscribe((v) => console.log(v));
lastUpdate1$.emit(1577923200000);
lastUpdate2$.emit(1653230659450);
Deriving
Deriving a signal consists of creating a new signal
that emits a value mapped from the source signal.
Example:
import {makeSignal, deriveSignal} from '@cdellacqua/signals';
const signal$ = makeSignal<number>();
const derived$ = deriveSignal(signal$, (n) => n + 100);
derived$.subscribe((v) => console.log(v));
signal$.emit(3);
Readonly signal
When you coalesce or derive a signal, you get back a ReadonlySignal<T>
.
This type lacks the emit
method.
A Signal<T>
is in fact an extension of a ReadonlySignal<T>
that adds the emit
method.
As a rule of thumb, it is preferable to pass around ReadonlySignal<T>
s,
to better encapsulate your signals and prevent unwanted emit
s.